package cn.demoncat.util.weixinwork;

import cn.demoncat.util.exception.BizRuntimeException;
import cn.demoncat.util.http.HttpUtil;
import cn.demoncat.util.http.entity.HttpRequest;
import cn.demoncat.util.lang.JsonUtil;
import cn.demoncat.util.lang.constant.StatusConstant;
import cn.demoncat.util.lang.constant.StringConstant;
import cn.demoncat.util.lang.entity.ListObj;
import cn.demoncat.util.lang.entity.ListStr;
import cn.demoncat.util.lang.entity.MapSo;
import cn.demoncat.util.lang.entity.MapSs;
import cn.demoncat.util.redis.util.RedisUtil;
import cn.demoncat.util.sec.SHA1Util;
import cn.demoncat.util.web.SpringUtil;
import cn.demoncat.util.weixinwork.config.WeixinWorkProperties;
import cn.demoncat.util.weixinwork.constant.WeixinWorkConstant;
import cn.demoncat.util.weixinwork.constant.WeixinWorkRedisConstant;
import cn.demoncat.util.weixinwork.entity.ProcessFormComponent;
import cn.demoncat.util.weixinwork.entity.ProcessFormComponentValue;
import cn.demoncat.util.weixinwork.event.WeixinWorkProcessInstanceChangeEvent;
import cn.demoncat.util.xml.XmlUtil;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Element;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 企业微信工具
 *
 * @author 延晓磊
 * @since 2023年03月28日
 */
@Component
public class WeixinWorkUtil {

    @Resource
    private WeixinWorkProperties weixinWorkProperties;
    @Resource
    private RedisUtil redisUtil;
    @Resource
    private SpringUtil springUtil;

    /**
     * 获取AccessToken
     *
     * 1、accessToken的有效期为7200秒，有效期内重复获取会返回相同结果，过期后获取会返回新的accessToken。
     * 2、开发者需要缓存accessToken，用于后续接口的调用；因为每个应用的accessToken是独立的，所以进行缓存时需要区分应用来进行存储。
     * 3、企业微信可能会出于运营需要，提前使access_token失效，开发者应实现access_token失效时重新获取的逻辑。
     *
     * @return accessToken
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String getAccessToken(){
        return redisUtil.getStr(WeixinWorkRedisConstant.ACCESS_TOKEN, () -> this.request(HttpRequest.get(WeixinWorkConstant.Api.ACCESS_TOKEN.getUrl()).name(WeixinWorkConstant.Api.ACCESS_TOKEN.getName()).addParam("corpid", weixinWorkProperties.getCorpId()).addParam("corpsecret", weixinWorkProperties.getSecret())).getString("access_token"));
    }

    /**
     * 订阅事件 - 参数校验
     *
     * 1、保存事件订阅配置时，由企业微信发起校验请求
     * 2、需在1秒内解密并返回消息明文
     *
     * @param signature 消息体签名
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @param body      消息体
     *
     * @return 消息明文
     *
     * @author 延晓磊
     * @since 2023年03月30日
     */
    public String check(String signature, String timestamp, String nonce, String body){
        // 校验签名：SHA1(join(sort([token,timestamp,nonce,body])))
        if (!signature.equals(SHA1Util.digest(ListStr.getInstance(weixinWorkProperties.getEventToken(), timestamp, nonce, body).stream().sorted().collect(Collectors.joining())))) {
            throw new BizRuntimeException("接收企业微信事件失败：参数签名错误");
        }
        // 解密消息：AES
        return aes(weixinWorkProperties.getCorpId(), weixinWorkProperties.getEventKey(), body);
    }

    /**
     * 订阅事件
     *
     * 1、触发订阅事件时，由企业微信发起通知请求，返回HTTP-200响应表示成功（间隔5秒重推3次）
     * 2、需要在企业微信中订阅事件，并实例化 cn.demoncat.util.weixinwork.event 中的 Event
     *
     * # 审批事件订阅
     * 1、配置内置【审批】应用的事件订阅，同时勾选自建应用：应用管理 > 审批 > API > 接收事件服务器 > 配置回调地址和密钥，勾选订阅的流程
     * 2、配置【自建应用】的事件订阅：应用管理 > 自建应用 > 接收消息-设置API接收 > 配置回调地址和密钥
     * 3、企业微信会同时推送到【审批】和【自建应用】，需要在程序中通过AgentId和processInstanceId进行过滤。
     *
     * @param signature 消息体签名
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @param body      消息体：<xml><Encrypt>密文</Encrypt></xml>
     *
     * @return 成功响应
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String callback(String signature, String timestamp, String nonce, String body) {
        // 读取XML
        Element element = XmlUtil.readXml(this.check(signature, timestamp, nonce, XmlUtil.readXml(body).getRootElement().element("Encrypt").getTextTrim())).getRootElement();
        // 校验来源（订阅事件的应用）
        if (weixinWorkProperties.getAgentId().equals(element.element("AgentID").getTextTrim())) {
            // 分发事件
            switch (element.element("Event").getTextTrim()) {
                case WeixinWorkProcessInstanceChangeEvent.EventType:
                    // 审批流程结束
                    Element processInstanceChangeElement = element.element("ApprovalInfo");
                    int processInstanceChangeStatus = Integer.parseInt(processInstanceChangeElement.element("SpStatus").getTextTrim()) - 1;
                    if (processInstanceChangeStatus > 0 && processInstanceChangeStatus < 4) {
                        springUtil.getBean(WeixinWorkProcessInstanceChangeEvent.class, event -> event.consumer(WeixinWorkProcessInstanceChangeEvent.Data.build(processInstanceChangeElement.element("SpNo").getTextTrim(), processInstanceChangeStatus)));
                    }
                    break;
                default:
                    break;
            }
        }
        return StringConstant.EMPTY;
    }


    /**
     * 工作流-创建流程表单（模板）
     *
     * 文档：https://developer.work.weixin.qq.com/document/path/97437
     *
     * 开通审批功能：应用管理 > 审批 > API > 获取数据权限-设置 > 勾选应用，使其可调用审批接口
     * 配置审批流程：应用管理 > 审批 > 模板管理 > 编辑审批模板 > 规则设置（审批流程）
     * 查找模板ID：应用管理 > 审批 > 模板管理 > 编辑审批模板 > URL的末尾PATH即模板ID
     *
     * @param name      表单名称：不得和现有模版名称重复，长度不得超过40个字符
     * @param components 表单控件
     *
     * @return template_id 表单模板Code，企业内唯一
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String createProcessForm(String name, List<ProcessFormComponent> components){
        return updateProcessForm(null, name, components);
    }

    /**
     * 工作流-修改流程表单（模板）
     *
     * 常见错误：模板名称重复、应用密钥错误、未配置IP白名单、未开通审批权限
     *
     * @param templateId    表单模板Code，为空时新增：创建表单时返回，管理后台/应用管理/审批/模板管理/编辑审批模板/URL的末尾PATH即模板ID
     * @param name          表单名称
     * @param components    表单控件
     *
     * @return template_id 表单模板Code，企业内唯一
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String updateProcessForm(String templateId, String name, List<ProcessFormComponent> components){
        // 表单
        List<Object> controls = new ArrayList<>(components.size());
        MapSo map = MapSo.getInstance(
                // ID
                "template_id", templateId,
                // 名称
                "template_name", MapSs.getInstance("text", name, "lang", "zh_CN"),
                // 控件
                "template_content", MapSo.getInstance("controls", controls)
        );
        // 控件
        int i = 1;
        for (ProcessFormComponent component : components) {
            controls.add(
                    MapSo.getInstance(
                            "property", MapSo.getInstance(
                                    // 控件类型
                                    "control", component.getType(),
                                    // 控件id：control-数字，如"Text-01"
                                    "id", component.getType() + StringConstant.BAR + i,
                                    // 控件名称
                                    "title", MapSs.getInstance("text", component.getName(), "lang", "zh_CN"),
                                    // 控件说明
                                    "placeholder", StringUtils.isBlank(component.getPlaceholder()) ? null : MapSs.getInstance("text", component.getPlaceholder(), "lang", "zh_CN"),
                                    // 是否必填：1是0否（默认）
                                    "require", component.getRequire()
                            )
                    )
            );
            i++;
        }
        if (StringUtils.isBlank(templateId)) {
            // 创建
            return this.request(WeixinWorkConstant.Api.CREATE_PROCESS_FORM, map).getString("template_id");
        }else{
            // 更新
            this.request(WeixinWorkConstant.Api.UPDATE_PROCESS_FORM, map);
            return templateId;
        }
    }

    /**
     * 工作流-发起流程实例
     *
     * 文档：https://developer.work.weixin.qq.com/document/path/91853
     *
     * 1、发起审批实例后，审批人可以通过“消息通知”和“工作台/审批”进行查看
     * 2、复用审批后台设置的审批流
     *
     * 常见错误：userId错误
     *
     * @param templateId    表单模板Code：创建表单时返回，管理后台/应用管理/审批/模板管理/编辑审批模板/URL的末尾PATH即模板ID
     * @param userId        审批发起人的userId：管理后台/通讯录/查看成员详情/账号即userId
     * @param values        审批参数（值可以为空，但控件必须传，且顺序与模板控件一致）
     *
     * @return instanceId   流程实例ID
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String startProcessInstance(String templateId, String userId, List<ProcessFormComponentValue> values){
        // 表单
        List<Object> controls = new ArrayList<>(values.size());
        List<Object> summaries = new ArrayList<>();
        MapSo map = MapSo.getInstance(
                // 模板
                "template_id", templateId,
                // 申请人
                "creator_userid", userId,
                // 审批人模式：：0 通过接口指定审批人、 1 用此模板在管理后台设置的审批流程
                "use_template_approver", StatusConstant.Common.YES,
                // 申请数据
                "apply_data", MapSo.getInstance("contents", controls),
                // 申请摘要：用于显示在审批通知卡片、审批列表的摘要信息，最多3行
                "summary_list", summaries
        );
        // 控件
        int i = 1;
        for (ProcessFormComponentValue value : values) {
            // 参数
            controls.add(
                    MapSo.getInstance(
                            // 控件类型
                            "control", value.getType(),
                            // 控件id：control-数字，如"Text-01"
                            "id", value.getType() + StringConstant.BAR + i,
                            // 控件值
                            "value", MapSs.getInstance("text", value.getValue(), "lang", "zh_CN")
                    )
            );
            // 概要
            if (i <= 3) {
                summaries.add(
                        MapSo.getInstance(
                            "summary_info", ListObj.getInstance(
                                        MapSo.getInstance("text", value.getName() + StringConstant.COLON_CHINESE + value.getValue(), "lang", "zh_CN")
                                )
                        )
                );
            }
            i++;
        }
        return this.request(WeixinWorkConstant.Api.START_PROCESS_INSTANCE, map).getString("sp_no");
    }

    /**
     * 工作流-查看流程实例
     *
     * @param instanceId    流程实例ID
     *
     * @return 流程实例详情
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public JSONObject getProcessInstance(String instanceId){
        return this.request(WeixinWorkConstant.Api.GET_PROCESS_INSTANCE, MapSo.getInstance("sp_no", instanceId)).getJSONObject("info");
    }

    /**
     * 工作流-撤销流程实例
     *
     * 1、企业微信不支持接口撤销
     *
     * @param instanceId    流程实例ID
     * @param remarks       终止说明
     *
     * @return 是否撤销成功
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public Boolean cancelProcessInstance(String instanceId, String remarks){
        throw new BizRuntimeException("请通过企业微信执行撤销");
    }




    /**
     * 构建审批流程表单控件 - 文本输入框
     *
     * @param name          标题
     * @param placeholder   占位文字
     * @param required      是否必填
     * @return
     *
     * @author 延晓磊
     * @since 2023年03月24日
     */
    public static ProcessFormComponent buildProcessFormTextComponent(String name, String placeholder, boolean required){
        ProcessFormComponent component = new ProcessFormComponent();
        component.setType(WeixinWorkConstant.FormControl.Text);
        component.setName(name);
        component.setPlaceholder(placeholder);
        component.setRequire(StatusConstant.Common.check(required));
        return component;
    }

    /**
     * 构建审批流程表单控件值 - 文本输入框
     *
     * @param name  标题
     * @param value 值
     * @return
     *
     * @author 延晓磊
     * @since 2023年03月27日
     */
    public static ProcessFormComponentValue buildProcessFormTextComponentValue(String name, String value){
        ProcessFormComponentValue component = new ProcessFormComponentValue();
        component.setType(WeixinWorkConstant.FormControl.Text);
        component.setName(name);
        component.setValue(value);
        return component;
    }



    /**
     * 发起请求
     *
     * @param request   {url,name,params}
     * @return
     *
     * @author 延晓磊
     * @since 2023年03月29日
     */
    private JSONObject request(HttpRequest request){
        JSONObject json = HttpUtil.request(request).asBean();
        switch (json.getString("errcode")) {
            case WeixinWorkConstant.Code.SUCCESS:
                // 成功
                return json;
            case WeixinWorkConstant.Code.ACCESS_TOKEN_EXPIRE:
                // access_token过期
                redisUtil.del(WeixinWorkRedisConstant.ACCESS_TOKEN.getKey());
                request.addParam("access_token", this.getAccessToken());
                return this.request(request);
            default:
                // 失败
                throw new BizRuntimeException("请求企业微信失败（"+ request.getName() +"）：" + json.getString("errmsg"));
        }
    }

    /**
     * 发起请求 - 附带access_token
     *
     * @param api
     * @param body
     * @return
     *
     * @author 延晓磊
     * @since 2023年03月29日
     */
    private JSONObject request(WeixinWorkConstant.Api api, Object body){
        return request(HttpRequest.post(api.getUrl()).name(api.getName()).addParam("access_token", this.getAccessToken()).json(JsonUtil.toStr(body)));
    }

    /**
     * AES解密
     *
     * @param cropId    归属企业
     * @param key       AES Key
     * @param text      密文
     *
     * @return 明文
     */
    private static String aes(String cropId, String key, String text) {
        // AES解码
        byte[] aesKey = Base64.decodeBase64(key + "=");
        byte[] original;
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
            original = cipher.doFinal(Base64.decodeBase64(text));
        } catch (Exception e) {
            throw new BizRuntimeException("接收企业微信事件失败：参数解密错误");
        }
        // 去除补位
        int pad = original[original.length - 1];
        if (pad < 1 || pad > 32) {
            pad = 0;
        }
        original = Arrays.copyOfRange(original, 0, original.length - pad);
        // 分离内容
        byte[] networkOrder = Arrays.copyOfRange(original, 16, 20);
        int xmlLength = 0;
        for (int i = 0; i < 4; i++) {
            xmlLength <<= 8;
            xmlLength |= networkOrder[i] & 0xff;
        }
        // 校验企业
        if (!cropId.equals(new String(Arrays.copyOfRange(original, 20 + xmlLength, original.length), StandardCharsets.UTF_8))) {
            throw new BizRuntimeException("接收企业微信事件失败：归属企业不匹配");
        }
        // 获取内容
        return new String(Arrays.copyOfRange(original, 20, 20 + xmlLength), StandardCharsets.UTF_8);
    }
}
